<?php
/*!
 * CSSOptimizer - CSS minify and optimize library
 * 
 * Copyright 2010, Anton Ilyuschenkov aka DriverX
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://driverx.su/license.html
 */

interface iCSSOptimizer {
	public function whitespace ();
	public function optimize ();
	public function write ();
	public function writeFile ($filename = '');
	public function getStat ();
	public function generateData ();
}

class clCSSOptimizer implements iCSSOptimizer {
	public $source = '';
	public $filename = false;
	public $cssText = '';
	public $rules = array();
	public $colors = array(
		'#f0ffff'	=>	'azure',
		'#f5f5dc'	=>	'beige',
		'#ffe4c4'	=>	'bisque',
		'#a52a2a'	=>	'brown',
		'#ff7f50'	=>	'coral',
		'#ffd700'	=>	'gold',
		'#808080'	=>	'gray',
		'#008000'	=>	'green', 
		'#4b0082'	=>	'indigo', 
		'#fffff0'	=>	'ivory',
		'#f0e68c'	=>	'khaki',
		'#faf0e6'	=>	'linen',
		'#800000'	=>	'maroon',
		'#000080'	=>	'navy',
		'#808000'	=>	'olive',
		'#ffa500'	=>	'orange',
		'#da70d6'	=>	'orchid',
		'#cd853f'	=>	'peru',
		'#ffc0cb'	=>	'pink',
		'#dda0dd'	=>	'plum',
		'#800080'	=>	'purple',
		'#f00'		=>	'red',
		'#fa8072'	=>	'salmon',
		'#a0522d'	=>	'sienna',
		'#c0c0c0'	=>	'silver',
		'#fffafa'	=>	'snow',
		'#d2b48c'	=>	'tan',
		'#008080'	=>	'teal',
		'#ff6347'	=>	'tomato',
		'#ee82ee'	=>	'violet',
		'#f5deb3'	=>	'wheat',
		
		'aliceblue'				=>	'#f0f8ff',
		'antiquewhite'			=>	'#faebd7',
		'aqua'					=>	'#0ff',
		'aquamarine'			=>	'#7fffd4',
		'black'					=>	'#000',
		'blanchedalmond'		=>	'#ffebcd',
		'blue'					=>	'#00f',
		'blueviolet'			=>	'#8a2be2',
		'burlywood'				=>	'#deb887',
		'cadetblue'				=>	'#5f9ea0',
		'chartreuse'			=>	'#7fff00',
		'chocolate'				=>	'#d2691e',
		'cornflowerblue'		=>	'#6495ed',
		'cornsilk'				=>	'#fff8dc',
		'cyan'					=>	'#0ff',
		'crimson'				=>	'#dc143c',
		'darkblue'				=>	'#00008b',
		'darkcyan'				=>	'#008b8b',
		'darkgoldenrod'			=>	'#b8860b',
		'darkgray'				=>	'#a9a9a9',
		'darkgreen'				=>	'#006400',
		'darkkhaki'				=>	'#bdb76b',
		'darkmagenta'			=>	'#8b008b',
		'darkolivegreen'		=>	'#556b2f',
		'darkorange'			=>	'#ff8c00',
		'darkorchid'			=>	'#9932cc',
		'darkred'				=>	'#8b0000',
		'darksalmon'			=>	'#e9967a',
		'darkseagreen'			=>	'#8fbc8f',
		'darkslateblue'			=>	'#483d8b',
		'darkslategray'			=>	'#2f4f4f',
		'darkturquoise'			=>	'#00ced1',
		'darkviolet'			=>	'#9400d3',
		'deeppink'				=>	'#ff1493',
		'deepskyblue'			=>	'#00bfff',
		'dimgray'				=>	'#696969',
		'dodgerblue'			=>	'#1e90ff',
		'firebrick'				=>	'#b22222',
		'floralwhite'			=>	'#fffaf0',
		'forestgreen'			=>	'#228b22',
		'fuchsia'				=>	'#f0f',
		'gainsboro'				=>	'#dcdcdc',
		'ghostwhite'			=>	'#f8f8ff',
		'goldenrod'				=>	'#daa520',
		'greenyellow'			=>	'#adff2f',
		'honeydew'				=>	'#f0fff0',
		'hotpink'				=>	'#ff69b4',
		'indianred'				=>	'#cd5c5c',
		'lavender'				=>	'#e6e6fa',
		'lavenderblush'			=>	'#fff0f5',
		'lawngreen'				=>	'#7cfc00',
		'lemonchiffon'			=>	'#fffacd',
		'lightblue'				=>	'#add8e6',
		'lightcoral'			=>	'#f08080',
		'lightcyan'				=>	'#e0ffff',
		'lightgoldenrodyellow'	=>	'#fafad2',
		'lightgrey'				=>	'#d3d3d3',
		'lightgreen'			=>	'#90ee90',
		'lightpink'				=>	'#ffb6c1',
		'lightsalmon'			=>	'#ffa07a',
		'lightseagreen'			=>	'#20b2aa',
		'lightskyblue'			=>	'#87cefa',
		'lightslategray'		=>	'#789',
		'lightsteelblue'		=>	'#b0c4de',
		'lightyellow'			=>	'#ffffe0',
		'lime'					=>	'#0f0',
		'limegreen'				=>	'#32cd32',
		'magenta'				=>	'#f0f',
		'mediumaquamarine'		=>	'#66cdaa',
		'mediumblue'			=>	'#0000cd',
		'mediumorchid'			=>	'#ba55d3',
		'mediumpurple'			=>	'#9370d8',
		'mediumseagreen'		=>	'#3cb371',
		'mediumslateblue'		=>	'#7b68ee',
		'mediumspringgreen'		=>	'#00fa9a',
		'mediumturquoise'		=>	'#48d1cc',
		'mediumvioletred'		=>	'#c71585',
		'midnightblue'			=>	'#191970',
		'mintcream'				=>	'#f5fffa',
		'mistyrose'				=>	'#ffe4e1',
		'moccasin'				=>	'#ffe4b5',
		'navajowhite'			=>	'#ffdead',
		'oldlace'				=>	'#fdf5e6',
		'olivedrab'				=>	'#6b8e23',
		'orangered'				=>	'#ff4500',
		'palegoldenrod'			=>	'#eee8aa',
		'palegreen'				=>	'#98fb98',
		'paleturquoise'			=>	'#afeeee',
		'palevioletred'			=>	'#d87093',
		'papayawhip'			=>	'#ffefd5',
		'peachpuff'				=>	'#ffdab9',
		'powderblue'			=>	'#b0e0e6',
		'rosybrown'				=>	'#bc8f8f',
		'royalblue'				=>	'#4169e1',
		'saddlebrown'			=>	'#8b4513',
		'sandybrown'			=>	'#f4a460',
		'seagreen'				=>	'#2e8b57',
		'seashell'				=>	'#fff5ee',
		'skyblue'				=>	'#87ceeb',
		'slateblue'				=>	'#6a5acd',
		'slategray'				=>	'#708090',
		'springgreen'			=>	'#00ff7f',
		'steelblue'				=>	'#4682b4',
		'thistle'				=>	'#d8bfd8',
		'turquoise'				=>	'#40e0d0',
		'white'					=>	'#fff',
		'whitesmoke'			=>	'#f5f5f5',
		'yellow'				=>	'#ff0',
		'yellowgreen'			=>	'#9acd32'
	);
	
	protected $options = array(
		'media'			=>	'all',
		'whitespace'	=>	true,
		'colors'		=>	true,
		'rgbcolors'		=>	true,
		'numeric'		=>	true
	);
	protected $isFile = false;
	protected $replacementColors = array(array(), array());
	public function __construct ($source = '', $flags = array()) {
		$source = (string) $source;
		$this->source = $source;
		if ($this->isFilename($source)) {
			$cssText = file_get_contents($source);
			if ($cssText === false) {
				throw new ErrorException('Can\'t open file '.$source.'!');
			}
			$this->filename = $source;
			$this->source = $cssText;
			$this->isFile = true;
		} else {
			$cssText = $source;
		}
		if (!$source) {
			throw new ErrorException('source is empty');
		}
		$this->cssText = $cssText;
		
		foreach ($this->colors as $pattern => $replacement) {
			//$this->replacementColors[0][] = '/(?<=^|[\s,(])'.$pattern.'(?=$|[\s,)])/i';
			$this->replacementColors[0][] = '/(?<=^|[^\w])'.$pattern.'(?=$|[^\w])/i';
			$this->replacementColors[1][] = $replacement;
		}
	}
	public function whitespace() {
		$cssText = $this->cssText;
		$cssText = preg_replace('/(?<!["\'\s])\s+(?!["\'\s])/', ' ', $cssText); // \s -> space
		$cssText = preg_replace('/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', '', $cssText); // Comments
		$cssText = preg_replace('/^\s+/', '', $cssText); // trim left
		$cssText = preg_replace('/\s+$/', '', $cssText); // trim right
		$cssText = preg_replace('/\s+(?=[{})!:])|(?<=[\s:;{},(])\s+/', '', $cssText); // between words and rules
		$this->cssText = $cssText;
	}
	public function generateData () {
		unset($this->rules);
		$this->rules = array();
		$found = array();
		preg_match_all('/@([^{};\s]+)(?:\s+([^{};]*))?(?:;|[{]((?:[^{}]+|[^{}]+[{][^@{}]*[}][^{}]*)+)[}])/', $this->cssText, $found, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
		if (count($found)) {
			for ($i = 0, $l = count($found); $i < $l; $i++) {
				$item = &$found[$i];
				if (!$i && $item[0][1]) {
					$this->addCascadeBlock('media', 'all', substr($this->cssText, 0, $item[0][1]));
				}
				$item[1][0] = strtolower($item[1][0]);
				
				if ($item[1][0] === 'media') {
					$item[2][0] = strtolower($item[2][0]);
				} else if ($item[1][0] === 'import') {
					$item[2][0] = preg_replace('/url[(]["\']?([^\s]+?)["\']?[)]/', '"\1"', $item[2][0]);
				}
				$this->addCascadeBlock($item[1][0], $item[2][0], isset($item[3]) ? $item[3][0] : null);
				
				if ($i + 1 < $l) {
					$nextItem = &$found[$i + 1];
				} else {
					unset($nextItem);
					$nextItem = null;
				}
				if (!$nextItem || $nextItem[0][1] != strlen($item[0][0]) + $item[0][1]) {
					$startIndex = strlen($item[0][0]) + $item[0][1];
					if ($nextItem) {
						$this->addCascadeBlock('media', 'all', substr($this->cssText, $startIndex, $nextItem[0][1] - $startIndex));
					} else if ($startIndex < strlen($this->cssText)) {
						$this->addCascadeBlock('media', 'all', substr($this->cssText, $startIndex));
					}
				}
			}
		} else {
			$this->addCascadeBlock('media', 'all', substr($this->cssText, 0));
		}
		unset($item, $nextItem);
		
		for ($i = 1, $l = count($this->rules); $i < $l; $i++) {
			$previous = &$this->rules[$i - 1];
			$current = &$this->rules[$i];
			if ($previous['type'] === 'media' && $current['type'] === 'media' && $previous['prop'] === $current['prop']) {
				$previous['block'] .= $current['block'];
				array_splice($this->rules, $i--, 1);
				$l--;
			}
		}
		unset($previous, $current);
		
		for ($i = 0, $l = count($this->rules); $i < $l; $i++) {
			$item = &$this->rules[$i];
			if ($item['block'] && preg_match('/[^:;]+:[^;]*;?/', $item['block'])) {
				$style = array();
				if (preg_match_all('/([^{}]+)[{]([^}]+?)[}]/', $item['block'], $style, PREG_SET_ORDER)) {
					for ($k = 0, $m = count($style); $k < $m; $k++) {
						$pairs = array();
						preg_match_all('/([^:;]+):([^;]+);?/', $style[$k][2], $pairs, PREG_SET_ORDER);
						for ($j = 0, $n = count($pairs); $j < $n; $j++) {
							$pairs[$j] = array(
								'rule'		=>	strtolower($pairs[$j][1]),
								'value'		=>	str_ireplace('!important', '', $pairs[$j][2]),
								'important'	=>	!is_bool(stripos($pairs[$j][2], '!important'))
							);
						}
						$style[$k] = array(
							'selector'	=>	$style[$k][1],
							'rules'		=>	$pairs
						);
					}
					$item['block'] = $style;
				}
			}
		}
		return;
		for ($i = 0, $l = count($this->rules); $i < $l; $i++) {
			$item = &$this->rules[$i];
			if ($item['type'] === "import" && $item['prop']) {
				$matches = array();
				if (preg_match('/^(?:url[(](?=["\']?[^)]+?["\']?[)]))?(?:(["\'])(?!\1+(?=\1)))?([^"\'()]+(?=(["\'][)]|[)]|["\'])))\3(?:\s+(.+))?/i', $item['prop'], $matches)) {
					try {
						$internalOptimizer = new clCSSOptimizer($matches[2]);
					} catch (Exception $e) {}
					$internalOptimizer->whitespace();
					$internalOptimizer->generateData();
					array_splice($this->rules, $i, 0, $internalOptimizer->rules);
					$i += count($internalOptimizer->rules);
					array_splice($this->rules, $i--, 1);
					$l = count($this->rules);
				}
			}
		}
	}
	private function addCascadeBlock ($type, $prop = '', $block = null) {
		$this->rules[] = array(
			'type'	=>	$type,
			'prop'	=>	$prop,
			'block'	=>	$block
		);
	}
	private function isFilename ($filename, $matches = array()) {
		return !!preg_match('/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/', $filename, $matches);
	}
	private function dataOptimizeColors ($pair) {
		$value = &$pair['value'];
		$value = preg_replace_callback('/(?<=^|[\s,])rgb\((\d{1,3}%?),(\d{1,3}%?),(\d{1,3}%?)\)(?=$|[\s,)])/', array($this, 'dataOptimizeColorsValueRGB'), $value);
		// TODO $value = preg_replace_callback('/(?<=^|[\s,])hsl\((\d{1,3}%?),(\d{1,3}%?),(\d{1,3}%?)\)(?=$|[\s,)])/', array($this, 'dataOptimizeColorsValueHSL'), $value);	
		$value = preg_replace('/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/i', '#\1\2\3', $value);
		$value = preg_replace($this->replacementColors[0], $this->replacementColors[1], $value);
		return $pair;
	}
	private function dataOptimizeColorsValueRGB ($matches) {
		$replacement = '#';
		for ($i = 1; $i <= 3; $i++) {
			$replacement .= sprintf('%02.2s', dechex(intval(strpos($matches[$i], '%') !== false ? round((100 - floatval($matches[$i]))/100 * 255) : $matches[$i])));
		}
		return $replacement;
	}
	private function dataOptimizeColorsValueHSL ($matches) {
		// TODO
	}
	private function dataOptimizeNumeric ($item) {
		$value = &$item['value'];
		$value = preg_replace('/(?<=^|[\s,(])(?:-?0*\.)?0+(?:[a-z]{2,}|%)?(?=$|[\s,)])/', '0', $value); // 0% 0px 0.0em .0% -> 0
		$value = preg_replace('/(?<=^|[\s,(])(-?\d+)\.0+(?=(?:[a-z]{2,}|%)?(?:$|[\s,)]))/', '\1', $value); // 2.0px 10.0em -> 2px 10em
		$value = preg_replace('/(?<=^|[\s,(])(-?\d*\.[\d]*[1-9]+)0+(?=(?:[a-z]{2,}|%)?(?:$|[\s,)]))/', '\1', $value); // 7.056000em -> 7.056em
		$value = preg_replace('/(?<=^|[\s,(\-])0+(?=\.[\d]+(?:[a-z]{2,}|%)?(?:$|[\s,)]))/', '', $value); // 0.8% -> .8%
		return $item;
	}
	private function dataOptimizeEachRule ($item) {
		switch ($item['rule']) {
			case 'font':
			case 'font-weight':
				$weights = array('/^normal(?=$|\s)/', '/^bold(?=$|\s)/');
				$values = array('400', '700');
				$item['value'] = preg_replace($weights, $values, $item['value']);
			break;
			case 'background':
			case 'background-position':
				if ($item['rule'] === 'background') {
					$item['value'] = preg_replace('/(?<=^|\s)(?:repeat|transparent|(?:0|top|left)[\s]+(?:0|top|left))(?=$|\s)/', '', $item['value']);
				}
				$item['value'] = preg_replace_callback('/(?<=^|\s)(?:(?:(-?[\d]+(?:%|[a-z]{2,})?|center|top|right|bottom|left)\s+(-?[\d]+(?:%|[a-z]{2,})?|center|top|right|bottom|left))|(-?[\d]+(?:%|[a-z]{2,})?|center|top|right|bottom|left)(?!\s+-?[\d]+(?:%|[a-z]{2,})?|center|top|right|bottom|left))/', array($this, 'dataOptimizeRuleBackgroundPosition'), $item['value']);
			break;
			case 'margin':
			case 'padding':
			case 'border-style':
			case 'border-color':
			case 'border-width':
				$nums = array();
				if (preg_match('/^([^\s]+)\s([^\s]+)(?:\s([^\s]+))?(?:\s([^\s]+))?/', $item['value'], $nums)) {
					array_splice($nums, 0, 1);
					$numnum = count($nums);
					while ($numnum > 1 && $nums[$numnum - 1] === $nums[($numnum == 2 ? 0 : $numnum - 3)]) {
						array_splice($nums, --$numnum, 1);
					}
					$item['value'] = implode(' ', $nums);
				}
			break;
		}
		return $item;
	}
	private function dataOptimizeRuleBackgroundPosition ($matches) {
		$replacement = array(
			'top'		=>	'0',
			'right'		=>	'100%',
			'bottom'	=>	'100%',
			'left'		=>	'0',
			'center'	=>	'50%'
		);
		if (isset($matches[3])) {
			switch ($matches[3]) {
				case 'left':
				case 'right':
				case 'center':
					return $replacement[$matches[3]];
				break;
			}
			return $matches[3];
		} else {
			$hor = $matches[1];
			$vrt = $matches[2];
			if ($hor === 'top' || $hor === 'bottom') {
				$tmp = $hor;
				$hor = $vrt;
				$vrt = $tmp;
			}
			if (isset($replacement[$hor])) {
				$hor = $replacement[$hor];
			}
			if (isset($replacement[$vrt])) {
				$vrt = $replacement[$vrt];
			}
			if ($hor === '50%' && $vrt !== '50%') {
				if ($vrt === '0') { // top
					return 'top';
				} else if ($vrt === '100%') { // bottom
					return 'bottom';
				}
			} else if ($vrt === '50%') {
				return $hor;
			}
			return $hor .' '. $vrt;
		}
	}
	public function optimize() {
		$this->whitespace();
		$this->generateData();
		
		for ($i = 0, $l = count($this->rules); $i < $l; $i++) {
			switch ($this->rules[$i]['type']) {
				case 'media':
					$blocks = &$this->rules[$i]['block'];
					for ($j = 0, $m = count($blocks); $j < $m; $j++) {
						$rules = &$blocks[$j]['rules'];
						$rules = array_map(array($this, 'dataOptimizeNumeric'), $rules);
						$rules = array_map(array($this, 'dataOptimizeColors'), $rules);
						$rules = array_map(array($this, 'dataOptimizeEachRule'), $rules);
					}
				break;
			}
		}
		unset($blocks, $rules);
		
/*
 * .test {margin: 0;}
 * .test2 {margin: 2px;}
 * .test2 {margin: 0;}
 * 
 * wrong
 * .test,test2{margin:0;}
 * .test2{margin:2px;}
 * 
 * right
 * .test2{margin:2px;}
 * .test,test2{margin:0;}
 */
		
		echo '<pre>';
		var_dump($this->rules);
		echo '</pre>';
		
		$this->cssText = $this->serializeData();
		return $this->cssText;
	}
	private function serializeData () {
		$result = '';
		for ($i = 0, $l = count($this->rules); $i < $l; $i++) {
			$item = &$this->rules[$i];
			if ($item['type'] !== 'media' || $item['prop'] !== 'all') {
				$result .= '@' . $item['type'];
				if ($item['prop']) {
					$result .= ' ' . $item['prop'];
				}
			}
			if ($item['block']) {
				$block = '';
				if (is_array($item['block'])) {
					for ($j = 0, $m = count($item['block']); $j < $m; $j++) {
						$arrblock = &$item['block'][$j];
						$n = count($arrblock['rules']);
						if ($n > 0) {
							$block .= $arrblock['selector'] . '{';
							for ($k = 0; $k < $n; $k++) {
								$rule = &$arrblock['rules'][$k];
								$block .= $rule['rule'] . ':' . $rule['value'];
								if ($rule['important']) {
									$block .= '!important';
								}
								if ($k + 1 != $n) {
									$block .= ';';
								}
							}
							$block .= '}';
						}
					}
				} else {
					$block = $item['block'];
				}
				if ($item['type'] !== 'media' || $item['prop'] !== 'all') {
					$result .= '{' . $block . '}';
				} else {
					$result .= $block;
				}
			} else {
				$result .= ';';
			}
		}
		return $result;
	}
	public function write () {
		echo $this->cssText;
	}
	public function writeFile ($filename = '') {
		if (!$filename && !$this->isFile) {
			throw new ErrorException('Can\'t save to file!');
		}
		if ($this->cssText === null) {
			throw new ErrorException('No optimized css source');
		}
		
		if (!$filename) {
			$filename = preg_replace('/\.(\w+)$/', '.min.\1', $this->filename);
		}
		
		$fp = fopen($filename, 'w');
		fwrite($fp, $this->cssText, strlen($this->cssText));
		fclose($fp);
	}
	public function getStat() {
		return array();
	}
}


$start = microtime(true);
$CSSOptimizer = new clCSSOptimizer('./tests/test2.css');
$CSSOptimizer->optimize();
$CSSOptimizer->writeFile();
echo microtime(true) - $start;

?>